<?php
/*'****************************************************************************\
    CHTML 0.2.1
 	Copyright (C) 2007, 2008 Alexey Ilyin

 	This library is free software; you can redistribute it and/or modify it
	under the terms of the GNU Lesser General Public License as published
	by the Free Software Foundation; either version 2.1 of the License, or
	(at your option) any later version.

 	This library is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

 	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

\******************************************************************************/

require_once 'util.inc.php';
require_once 'cprops.inc.php';

/**
 *  @mainpage CHTML
 *  CHTML is a HTML manipulation library based on PHP5 DOM extension.
 *
 *  @section intro_sec Introduction
 *
 *  This is the introduction.
 *
 *  @section section_1 ...
 *
 *  @subsection step1 ...
 *
 *  etc...
 */

abstract class CMarkup extends CProps {

    abstract function DOM ();
    abstract function XPath ();
    abstract function node ();
    abstract function source ($root = NULL);

    /**
     *  Formats and returns element $name.
     *  Change nothing by default.
     *  @remarks    It needed to cope with PHP
     *              non-standard name[] of form elements
     */
    abstract protected static function parseName ($name);
    /*{
        user_error(get_class(), E_USER_ERROR);
    }*/
    /**
     *  Just a shortcut for $this->XPath()->query().
     */
    function query ($query) {
        return $this->XPath()->query($query, $this->node());
    }
    /**
     *  Creates and returns new C<TagName> element.
     *  @param  $tagname TagName; C<TagName> must extend CMarkupElement
     *  @param  $attr   attribute array for $mixed  => TagName
     *  @param  $inner  inner content (if any) for $mixed  => TagName
     *  @see    createElement()
     */
    function newElement($tagname, $attr = array(), $inner = NULL) {
        $node = isset($inner) ?
            $this->DOM()->createElement($tagname, $inner) :
            $this->DOM()->createElement($tagname);
        foreach ($attr as $key => $value) {
            $node->setAttribute($key, $value);
        }
        return $this->createElement($node);
    }
    /**
     *  Accepts/Creates new C<TagName> element and
     *  appends it to $this->node(), then returns.
     *  @param  $mixed  => CMarkupElement
     *  @param  $mixed  => TagName; C<TagName> must extend CMarkupElement
     *  @param  $attr   attribute array for $mixed => TagName
     *  @param  $inner  inner content (if any) for $mixed => TagName
     *  @see    newElement()
     */
    function append ($mixed, $attr = array(), $inner = NULL) {
        if (!($mixed instanceof CMarkupElement)) {
            $mixed = $this->newElement($mixed, $attr, $inner);
        }
        $this->node()->appendChild($mixed->node());
        return $mixed;
    }
    /**
     *  Clones and returns accepted/found element.
     *  @see    createElement()
     */
    function cloneElement($mixed, $deep = TRUE) {
        if (!($mixed instanceof CMarkupElement)) {
            $mixed = $this->element($mixed, 0);
        }
        return $this->createElement($mixed->node()->cloneNode($deep));
    }

    function cloneElements ($callback, $arg) {
        $elem = NULL;
        while ($elem = call_user_func_array($callback, 
            array($this, $elem, &$arg))) {
            $elem = $this->append($this->cloneElement($elem));
        }
    }
    /**
     *  Calls C<TagName>::create($node, $this) factory function.
     *  If C<nodeName> exists, it must extend CMarkupElement,
     *  otherwise, CElement is used by default.
     */
    protected function createElement ($node) {
        $class = 'c'.$node->nodeName;
        return call_user_func(array(class_exists($class) ?
            $class : 'CElement', 'create'), $this, $node);
    }
    /**
     *  The element cache.
     */
    protected $cached = array();
    /**
     *  Selects an element and possibly applies the $method to optional
     *  list of arguments.
     *  @param $selector => name | [name]\#id | [name].class | [name]~query.
     *  @param $method   if TRUE/FALSE, check if the elements[name] exists.
     *  @param $method   if integer, return nth element of the list.
     *  @code
     *  'abcd'   //  element by name (the same as *[@name="abcd"];
     *  '#abcd'  //  element by id (the same as *[@id="abcd"];
     *  '.abcd'  //  element by class (the same as *[@class="abcd"];
     *  '~abcd'  //  element by tagname <abcd>;
     *  @endcode
     */
    function element ($selector, $method = NULL) {
        preg_match('/^\s*([^\s~#\.]*)\s*([~#\.])?\s*(.*)\s*$/',
            $selector, $ex);
        if ($ex[1]) {
            //  the name part is present
            $name = $ex[1];
            //  test the special case of $name => key[]
            $key = $this->parseName($name);
            list($key, $idx) = each($key);
            //  if $idx is NOT empty, the $name that is
            //  key[idx] identifies an element uniquely
            if ($idx) $key = $name; 
        }
        else {
            //  no name part
            $name = $key = $ex[3];
        }
        assert('!empty($key)');
        switch ($ex[2]) {
        case '~' :
            $query = $ex[3];
            break;
        case '#' :
            $query = ".//*[normalize-space(@id)='$ex[3]']";
            break;
        case '.' :
            $query = ".//*[normalize-space(@class)='$ex[3]']";
            break;
        default :
            $query = ".//*[normalize-space(@name)='$ex[1]']";
            break;
        }
        $key = $ex[2].$key;
        
        if (array_key_exists($key, $this->cached)) {
            if (is_bool($method)) {
                return $method == TRUE;
            }
            $elem = $this->cached[$key];
        }
        else {
            $elem = $this->apply(array($this,'createElement'),
                $this->query($query));
            if (is_bool($method)) {
                return $method == !!$elem;
            }
            $this->cached[$key] = $elem;
        }
        if (!isset($method))
            return $elem;
        elseif (is_int($method))
            return $elem[$method];
        elseif (!$elem)
            throw new Exception("Element $selector not found");
        else {
            $args = func_get_args();
            array_shift($args); array_shift($args);
            return $this->apply(array($method), $elem, $args);
        }
    }
    /**
     *  Applies $callback($args) to $mixed data.
     *  Special case: $callback => array('method')
     *      calls $mixed->method.
     */
    static function apply ($callback, $mixed, $args = array()) {
        if (is_array($mixed) || $mixed instanceof DOMNodeList) {
            $result = array();
            foreach ($mixed as $item) {
                $value = self::apply($callback, $item, $args);
                if (isset($value)) $result[] = $value;
            }
            return $result;
        }
        if (is_array($callback) && count($callback) == 1) {
            return call_user_func_array(array($mixed,
                reset($callback)), $args); // special case
        }
        else {
            array_unshift($args, $mixed);
            return call_user_func_array($callback, $args);
        }
    }

    protected function sourceList_ ($nodes) {
        $result = '';
        foreach ($nodes as $node) {
            $result .= $this->source($node);
        }
        return $result;
    }

    function __toString () {
        return $this->source();
    }
    /**
     *  If $arg is single-element array, return that element.
     *  Otherwise return $arg or NULL.
     *  @todo move to util pack.
     */
    static function unmix ($arg) {
        if (is_array($arg) && count($arg) < 2) {
            return count($arg) == 1 ? reset($arg) : NULL;
        }
        return $arg;
    }
    /**
     *  If $var is array, append $value to it.
     *  Otherwise, if $var is set, convert it to array and append $value.
     *  Otherwise, just assign $value to $var.
     *  @todo move to util pack.
     */
    static function setmix(&$var, $value) {
        if (is_array($var))
            $var[] = $value;
        else if (isset($var))
            $var = array($var, $value);
        else
            $var = $value;
    }
    /**
     *  @todo move to util pack.
     */
    static function newInstance ($class,
        $arg1 = NULL, $arg2 = NULL, $arg3 = null) {
        return new $class($arg1, $arg2, $arg3);
    }
    /**
     *  @todo move to util pack.
     */
    function print_ ($arg) {
        echo $arg;
    }
    /**
     *  @todo move to util pack.
     */
    function print_list () {
        foreach (func_get_args() as $arg) {
            if (is_scalar($arg))
                $this->print_($arg);
            else if (is_callable($arg))
                $this->print_($arg());
            else if (is_object($arg))
                $this->print_($arg->__toString());
            else if (is_array($arg))
                $this->print_list($arg);
        }
    }
}

class CXMLDocument extends CMarkup {
	protected $DOM;
	protected $XPath;

	function CXMLDocument ($xml = NULL, $charset = NULL) {
        $this->DOM = new DOMDocument();
        $this->DOM->preserveWhiteSpace = FALSE;
        $this->DOM->formatOutput = TRUE;
        if ($xml) $this->load($xml, $charset);
	}

    function load ($xml, $charset = NULL) {
        $this->DOM->loadXML($xml);
        $this->afterLoad();
    }

    function loadFrom ($uri, $charset = NULL) {
        $this->DOM->load($uri);
        $this->afterLoad();
    }

    protected function afterLoad () {
        $this->DOM->normalize();
        $this->XPath = new DOMXPath($this->DOM);
    }

    function DOM    () { return $this->DOM; }
    function XPath  () { return $this->XPath; }
    function node   () { return $this->DOM->documentElement; }

    protected static function parseName ($name) {
        assert('isset($name)');
        return array($name => NULL);
    }

    function traverse ($callback) {
        $this->traverse_($callback, $this->node());
    }

    protected function traverse_ ($callback, $mixed, $level=0, $index=0) {
        if ($mixed instanceof DOMNodeList) {
            for ($i = 0; $i < $mixed->length; $i++) {
               	$this->traverse_($callback, $mixed->item($i), $level, $i);
            }
        }
        else if ($mixed instanceof DOMNode) {
            call_user_func($callback, $mixed, $level, $index);
            $this->traverse_($callback, $mixed->childNodes, $level+1);
        }
    }

    function debug ($node, $level, $index) {
        while (0 < $level--) {
            echo "    ";
        }
        echo "$index: $node->nodeName\r\n";
    }

    function source ($root = NULL) {
        return $this->DOM->saveXML($root);
    }
}

/////// ELEMENTS ///////////////////////////////////////////////////////////////

abstract class  CMarkupElement extends CMarkup
    implements  ArrayAccess
{

    protected $node;
    protected $base;

    function CMarkupElement ($base, $node) {
        $this->node = $node;
        $this->base = $base;
        assert('$this->node()->ownerDocument == $this->base->DOM()');
    }

    abstract static function create($base, $node);

    function DOM   () { return $this->base->DOM(); }
    function XPath () { return $this->base->XPath(); }
    function node  () { return $this->node; }

    function get ($prop) {
        return method_exists($this, "get_$prop") ?
             $this->{"get_$prop"}() : $this->$prop;
    }

    function set ($prop, $value) {
        return method_exists($this, "set_$prop") ?
            $this->{"set_$prop"}($value) : $this->$prop = $value;
    }

    function __get ($name) {
        return $this->node->{$name};
    }
    function __set ($name, $value) {
        return $this->node->{$name} = $value;
    }
    function __isset ($name) {
        $value = $this->node->{$name};
        return isset($value);
    }
    function __unset ($name) {
        $this->node->{$name} = NULL;
    }

    function offsetGet ($name) {
        return $this->getAttribute($name);
    }
    function offsetSet ($name, $value) {
        $this->setAttribute($name, $value);
        return $value;
    }
    function offsetExists ($name) {
        return $this->hasAttribute($name);
    }
    function offsetUnset ($name) {
        $this->removeAttribute($name);
    }

    function getAttribute ($name) {
        return $this->node->getAttribute($name);
    }
    function setAttribute ($name, $value) {
        $this->node->setAttribute($name, $value);
        return $value;
    }
    function hasAttribute ($name) {
        return $this->node->hasAttribute($name);
    }
    function removeAttribute ($name) {
        $this->node->removeAttribute($name);
    }
    /**
     *  Tries to delegate the $method($args) to $this->base.
     */
    function __call($method, $args) {
        return call_user_func_array(array($this->base,
            $method), $args);
    }

    function removeChildren ($nodes = NULL) {
        $node = $this->node();
        if ($nodes) {
            if (!is_array($nodes)) {
                $nodes = array($nodes);
            }
            foreach ($nodes as $child) {
                if ($child instanceof CMarkupElement) {
                    $node->removeChild($child->node());
                }
                else if ($child instanceof DOMElement) {
                    $node->removeChild($child);
                }
            }
        }
        else while ($node->firstChild) {
            $node->removeChild($node->firstChild);
        }
    }

    function appendChildren ($nodes) {
        if ($nodes) {
            $node = $this->node();
            if (!is_array($nodes)) {
                $nodes = array($nodes);
            }
            foreach ($nodes as $child) {
                if ($child instanceof CMarkupElement) {
                    $node->appendChild($child->node());
                }
                else if ($child instanceof DOMElement) {
                    $node->appendChild($child);
                }
            }
        }
        return $this->get_children();
    }

    function insertChildren ($nodes, $before) {
        if ($nodes) {
            $node = $this->node();
            if (!is_array($nodes)) {
                $nodes = array($nodes);
            }
            foreach ($nodes as $child) {
                if ($child instanceof CMarkupElement) {
                    $child->node = $node->insertBefore($child->node(), $before);
                }
                else if ($child instanceof DOMElement) {
                    $node->insertBefore($child, $before);
                }
            }
        }
        return $this->get_children();
    }

    function set_children ($nodes) {
        $this->removeChildren();
        $this->appendChildren($nodes);
        return $this->get_children();
    }

    function get_children () {
        /// @todo
        //???//return $this->node()->childNodes;
    }

    function source ($root = NULL) {
        return $this->DOM()->saveXML($root ?
            $root : $this->node);
    }
}

?>